using System;
using System.Reflection;
using Db4objects.Db4o;
using Db4objects.Db4o.Linq;
using SharedObjects;
using System.Linq;
using TheGameLib;

namespace TheGame.Server
{
	/// <summary>
	/// This class "executes" programs, ie. "software" that the player uses while playing, gets "executed" here, with
	/// checks for permissions, the effect of the "software" etc.
	/// 
	/// This class will be invoke the method specified in the program's "MethodName" property.
	/// All possible MethodName values are defined here as real callable class methods, such as CopyFile etc
	/// If a program uses a MethodName that this class doesn't implement, the program will not work.
	/// 
	/// If a program gets executed, the MethodName will be Invoke()'d, and the program's actual behavior will be executed.
	/// The program's behavior can be virtually anything that can be programmed on the server side.
	/// As long as a program gets defined in the database specifying the correct MethodName, it will work.
	/// Note that you also need to define a program on the client side with the same Guid as on the server.
	/// </summary>
	public class ProgramExecutor
	{
		protected IObjectContainer Db;
		private int _version;
		private readonly Guid _sessionId;
		private AgentSessionObject _aso;

		public ProgramExecutor(Guid sessionId)
		{
			_sessionId = sessionId;
		}

		public virtual bool Execute(ref IObjectContainer db, Program program, object[] methodParameters)
		{
			_version = program.Version;
			_aso = World.Instance.GetAgentSession(_sessionId);
			if (_aso == null)
				return false;

			MethodInfo mi = typeof(ProgramExecutor).GetMethod(program.MethodName);
			if (mi == null)
				return false;
			Db = db;
			if (mi.ReturnType != typeof(bool))
				return false;
			if (mi.GetParameters().Count() != methodParameters.Count())
				return false;

			return (bool)mi.Invoke(this, methodParameters);
		}

		public virtual ComputerSystemAccount Execute(ref IObjectContainer db, Bypasser bypasser, object[] methodParameters)
		{
			_version = bypasser.Version;
			_aso = World.Instance.GetAgentSession(_sessionId);
			if (_aso == null)
				return null;

			MethodInfo mi = typeof(ProgramExecutor).GetMethod(bypasser.MethodName);
			if (mi == null)
				return null;
			Db = db;
			if (mi.ReturnType != typeof(ComputerSystemAccount))
				return null;
			if (mi.GetParameters().Count() != methodParameters.Count())
				return null;

			return (ComputerSystemAccount)mi.Invoke(this, methodParameters);
		}

		#region normal programs
		[ProgramMethod]
		public bool CopyFile(Guid sourceConnectionId, Guid targetConnectionId, ulong sourceFileId, ulong targetDirectoryId)
		{
			if (!IsSourceTargetConnectionValid(sourceConnectionId, targetConnectionId))
				return false;

			//actual program code
			SystemFile source = (from SystemFile f in Db where f.Id == sourceFileId select f).FirstOrDefault();
			if (source == null)
				return false;
			
			//if the source is remote, check that the file belongs to the remote system or return false.
			if (!CheckIsFileValid(sourceConnectionId, sourceFileId))
				return false;

			ulong parentDirId = (from SystemFile f in Db where f.Id == sourceFileId select f.ParentId).FirstOrDefault();
			if (parentDirId == targetDirectoryId)
				return false;

			if (!CheckIsDirectoryValid(targetConnectionId, targetDirectoryId))
				return false;

			if(targetConnectionId == Guid.Empty)
			{
				if (!HasEnoughMemory(source.Size))
					return false;
			}

			SystemDirectory target = (from SystemDirectory d in Db where d.Id == targetDirectoryId select d).FirstOrDefault();
			if (target == null)
				return false;

			SystemFile newFile = source.Copy(Db);// new SystemFile();
			if (newFile == null)
				return false;
			
			newFile.ParentId = target.Id;

			AgentConnection targetconn = _aso.GetAgentConnection(targetConnectionId);
			if (targetconn != null) //target is remote
			{
				ComputerSystem targetSystem = (from ComputerSystem cs in Db where cs.Id == targetconn.TargetSystemId select cs).FirstOrDefault();
				if (targetSystem != null)
					targetSystem.Log(Db, targetconn.GetConnectedHostId(targetSystem.Id), LoggingAction.FileCreated, source.Name);
			}
			
			AgentConnection sourceconn = _aso.GetAgentConnection(sourceConnectionId);
			if (sourceconn != null) //source is remote
			{
				ComputerSystem sourceSystem = (from ComputerSystem cs in Db where cs.Id == sourceconn.TargetSystemId select cs).FirstOrDefault();
				if (sourceSystem != null)
					sourceSystem.Log(Db, sourceconn.GetConnectedHostId(sourceSystem.Id), LoggingAction.FileRead, source.Name);
			}
			Db.PersistEntity(target);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool DeleteFile(Guid sourceConnectionId, ulong sourceFileId)
		{
			if (!CheckIsFileValid(sourceConnectionId, sourceFileId))
				return false;

			SystemFile source = (from SystemFile f in Db where f.Id == sourceFileId && !f.IsDeleted select f).FirstOrDefault(); 
			
			if (source == null)
				return false;
			
			if (source.IsLocked)
				return false;

			source.IsDeleted = true;
			source.DeletionTimeStamp = DateTime.Now;
			source.DeleteLevel = _version;
			ComputerSystem target = (from ComputerSystem cs in Db where cs.RootDirectory != null && cs.RootDirectory.Id == source.ParentId select cs).FirstOrDefault();

			AgentConnection conn = _aso.GetAgentConnection(sourceConnectionId);
			if (target != null && conn != null)
				target.Log(Db, conn.GetConnectedHostId(target.Id), LoggingAction.FileDeleted, source.Name);

			Db.Store(source);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool UndeleteFile(Guid sourceConnectionId, ulong sourceFileId)
		{
			if (!CheckIsFileValid(sourceConnectionId, sourceFileId))
				return false;

			SystemFile source = (from SystemFile f in Db where f.Id == sourceFileId select f).FirstOrDefault();

			if (source == null || !source.IsDeleted)
				return false;

			var parent = (from SystemDirectory d in Db where d.Id == source.ParentId select d).FirstOrDefault();
			if (parent == null)
				return false;

			if (source.DeleteLevel > _version)
				return false;

			ComputerSystem target = (from ComputerSystem cs in Db where cs.RootDirectory != null && cs.RootDirectory.Id == parent.Id select cs).FirstOrDefault();
			AgentConnection conn = _aso.GetAgentConnection(sourceConnectionId);
			if (target != null && conn != null)
				target.Log(Db, conn.GetConnectedHostId(target.Id), LoggingAction.FileRestored, source.Name);

			source.IsDeleted = false;
			source.DeleteLevel = 0;
			source.DeletionTimeStamp = new DateTime();
			Db.Store(source);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool DecompressFile(Guid sourceConnectionId, ulong sourceFileId)
		{
			SystemFile file = (from SystemFile f in Db where f.Id == sourceFileId select f).FirstOrDefault();
			if (file == null)
				return false;
			
			if (!CheckIsFileValid(sourceConnectionId, sourceFileId))
				return false;

			//decompression not possible if version below compression level
			if (_version < file.CompressionLevel || file.CompressionLevel == 0)
				return false;

			//decompression not possible, if result overflows local memory capacity.
			int totalMemory = _aso.AgentInDb.Gateway.GetTotalMemory();
			int currentlyUsed = _aso.AgentInDb.Gateway.ComputerSystem.RootDirectory.GetUsedMemory(Db);
			int usedAfterProcess = currentlyUsed - file.DisplaySize + file.Size;
			if (usedAfterProcess > totalMemory)
				return false;

			file.DisplaySize = file.Size;
			file.CompressionLevel = 0;
			Db.Store(file);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool CompressFile(Guid sourceConnectionId, ulong sourceFileId)
		{
			SystemFile file = (from SystemFile f in Db where f.Id == sourceFileId select f).FirstOrDefault();
			if (file == null)
				return false;

			if (!CheckIsFileValid(sourceConnectionId, sourceFileId))
				return false;

			if (file.CompressionLevel > 0)
				return false;

			file.DisplaySize = (int)(file.Size / GetCompressionModifier());
			if (file.DisplaySize < 1)
				file.DisplaySize = 1;

			file.CompressionLevel = _version;
			Db.Store(file);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool DecryptFile(Guid sourceConnectionId, ulong sourceFileId)
		{
			SystemFile file = (from SystemFile f in Db where f.Id == sourceFileId select f).FirstOrDefault();
			if (file == null)
				return false;

			if (!CheckIsFileValid(sourceConnectionId, sourceFileId))
				return false;

			//decompression not possible if version below compression level
			if (_version < file.EncryptionLevel || file.EncryptionLevel == 0)
				return false;

			file.EncryptionLevel = 0;
			Db.Store(file);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool EncryptFile(Guid sourceConnectionId, ulong sourceFileId)
		{
			SystemFile file = (from SystemFile f in Db where f.Id == sourceFileId select f).FirstOrDefault();
			if (file == null)
				return false;

			if (!CheckIsFileValid(sourceConnectionId, sourceFileId))
				return false;

			if (file.EncryptionLevel > 0)
				return false;

			file.EncryptionLevel = _version;
			Db.Store(file);
			Db.Commit();
			return true;
		}
		
		[ProgramMethod]
		public bool CreateRainbowTable(Guid targetConnectionId, ulong targetDirectory)
		{
			SystemDirectory target = (from SystemDirectory sd in Db where sd.Id == targetDirectory select sd).FirstOrDefault();
			if (target == null)
				return false;
			if (!CheckIsDirectoryValid(targetConnectionId, targetDirectory))
				return false;

			if(targetConnectionId == Guid.Empty)
			{
				if (!HasEnoughMemory(_version * 10))
					return false;
			}

			SystemFile rbtFile = new SystemFile
			{
				Id = Guid.NewGuid().ToLong(),
				CompressionLevel = 0,
				Content = null,
				CopyOf = null,
				DisplaySize = _version * 10,
				EncryptionLevel = 0,
				Name = string.Format("{0}.RBT", BitConverter.ToString(Guid.NewGuid().ToByteArray())),
				Size = _version * 10,
				Time = World.Instance.WorldTime,
				Type = FileType.RainbowtableFile,
				ParentId = target.Id
			};
			Db.PersistEntity(rbtFile);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool DigitizeFingerprint(Guid targetConnectionId, ulong targetDirectory)
		{
			return CreateCredentialFile(targetDirectory, targetConnectionId, FileType.Fingerprint);
		}

		[ProgramMethod]
		public bool DigitizeRetina(Guid targetConnectionId, ulong targetDirectory)
		{
			return CreateCredentialFile(targetDirectory, targetConnectionId, FileType.Retina);
		}

		[ProgramMethod]
		public bool DigitizeDNA(Guid targetConnectionId, ulong targetDirectory)
		{
			return CreateCredentialFile(targetDirectory, targetConnectionId, FileType.Dna);
		}

		[ProgramMethod]
		public bool DeleteLog(Guid targetConnectionId, ulong targetLogId)
		{
			if (!CheckIsLogValid(targetConnectionId, targetLogId))
				return false;
			ComputerSystemLog log = (from ComputerSystemLog csl in Db where csl.Id == targetLogId select csl).FirstOrDefault();
			if (log == null)
				return false;
			ComputerSystem cs = (from ComputerSystem c in Db where c.Id == log.LoggingComputerSystemId select c).FirstOrDefault();
			if(cs == null)
				return false;

			log.IsDeleted = true;
			log.DeleteLevel = _version;
			log.DeletionTimeStamp = DateTime.Now;

			//if log deleter version > target system security level then dont log the deletion action
			if(_version <= cs.DifficultyLevel)
				cs.Log(Db, _aso.GetAgentConnection(targetConnectionId).GetConnectedHostId(cs.Id), LoggingAction.LogDeleted, null);

			Db.PersistEntity(log);
			Db.Commit();
			return true;
		}

		[ProgramMethod]
		public bool UndeleteLog(Guid targetConnectionId, ulong targetLogId)
		{
			if (!CheckIsLogValid(targetConnectionId, targetLogId))
				return false;
			ComputerSystemLog log = (from ComputerSystemLog csl in Db where csl.Id == targetLogId select csl).FirstOrDefault();
			if(log == null)
				return false;
			ComputerSystem cs = (from ComputerSystem c in Db where c.Id == log.LoggingComputerSystemId select c).FirstOrDefault();
			if(cs == null)
				return false;
			if(log.DeleteLevel > _version)
				return false;
			log.IsDeleted = false;
			log.DeleteLevel = 0;
			log.DeletionTimeStamp = new DateTime();
			if(_version <= cs.DifficultyLevel)
				cs.Log(Db, _aso.GetAgentConnection(targetConnectionId).GetConnectedHostId(cs.Id), LoggingAction.LogRestored, null);
			Db.PersistEntity(log);
			Db.Commit();
			return true;
		}
		#endregion

		#region Bypassers
		[ProgramMethod]
		public ComputerSystemAccount BypassUsernamePassword(ulong systemId, ulong secSysId, Guid connectionId, string username = "")
		{
			return BypassSecSys(systemId, secSysId, connectionId, username);
		}

		[ProgramMethod]
		public ComputerSystemAccount BypassFingerprint(ulong systemId, ulong secSysId, Guid connectionId)
		{
			return BypassSecSys(systemId, secSysId, connectionId);
		}

		[ProgramMethod]
		public ComputerSystemAccount BypassRetinal(ulong systemId, ulong secSysId, Guid connectionId)
		{
			return BypassSecSys(systemId, secSysId, connectionId);
		}

		[ProgramMethod]
		public ComputerSystemAccount BypassDNA(ulong systemId, ulong secSysId, Guid connectionId)
		{
			return BypassSecSys(systemId, secSysId, connectionId);
		}

		[ProgramMethod]
		public ComputerSystemAccount BypassPSEC(ulong systemId, ulong secSysId, Guid connectionId)
		{
			return BypassSecSys(systemId, secSysId, connectionId);
		}

		[ProgramMethod]
		public ComputerSystemAccount BypassTBPA(ulong systemId, ulong secSysId, Guid connectionId)
		{
			return BypassSecSys(systemId, secSysId, connectionId);
		}

		private ComputerSystemAccount BypassSecSys(ulong systemId, ulong secSysId, Guid connectionId, string username = null)
		{
			AgentConnection conn = _aso.GetAgentConnection(connectionId);
			if (conn == null)
				return null;
			ComputerSystem cs = (from ComputerSystem c in Db where c.Id == systemId select c).FirstOrDefault();
			if (cs == null)
				return null;
			SecuritySystem secSys = (from SecuritySystem ss in cs.SecuritySystem where ss.Id == secSysId select ss).FirstOrDefault();
			if(secSys == null)
				return null;
			
			// note: since the the ss difficulty is always the same due to procedural generation, we use the CS's difficulty for chance calculation.
			double chance;
			if (secSys.CredentialType == CredentialType.UsernamePassword)
			{
				if (!GetBypassChance(25, cs.DifficultyLevel, _version, out chance))
				{
					if (new Random(Environment.TickCount).NextDouble()*100 < 100 - chance)
						conn.Trace(Db);
					cs.Log(Db, conn.GetConnectedHostId(conn.TargetSystemId), LoggingAction.BypassFailed, secSys.Name);
					return null;
				}
			}
			else
			{
				if (!GetBypassChance(25, cs.DifficultyLevel, _version, out chance))
				{
					if (new Random(Environment.TickCount).NextDouble() * 100 < 100 - chance)
						conn.Trace(Db);
					cs.Log(Db, conn.GetConnectedHostId(conn.TargetSystemId), LoggingAction.BypassFailed, secSys.Name);
					return null;
				}
			}

			//this also means, the higher the secLevel-to-Version ratio (1:6 best), the less the chance of setting off a trace.
			if (new Random(Environment.TickCount).NextDouble() * 100 < 100 - chance)
				conn.Trace(Db);
			
			ComputerSystemAccount csa;
			if(!string.IsNullOrEmpty(username))
				csa = (from ComputerSystemAccount a in cs.Accounts where a.Username == username select a).FirstOrDefault();
			else
				csa = (from ComputerSystemAccount a in cs.Accounts where a.IsDefault select a).FirstOrDefault();

			if (csa == null)
			{
				cs.Log(Db, conn.GetConnectedHostId(conn.TargetSystemId), LoggingAction.BypassFailed, secSys.Name);
				return null;
			}
			cs.Log(Db, conn.GetConnectedHostId(conn.TargetSystemId), LoggingAction.Bypass, secSys.Name);
			return csa;
		}

		private bool GetBypassChance(double baseChanceOfFailure, double difficulty, double bypassVersion, out double calculatedChance)
		{
			double chanceOfFailureMultiplier = difficulty/bypassVersion; //(ss:bp) 1:1 = 1.0, 10:1 = 10, 1:6 = 0.17
			double retval = 100 - (baseChanceOfFailure * chanceOfFailureMultiplier);
			calculatedChance = retval;
			double diceValue = new Random(Environment.TickCount).NextDouble()*100;
			if (diceValue <= retval)
				return true;
			return false;
		}
		#endregion

		#region helper functions
		private bool CreateCredentialFile(ulong targetDirectory, Guid targetConnectionId, FileType fileType)
		{
			SystemDirectory target = (from SystemDirectory sd in Db where sd.Id == targetDirectory select sd).FirstOrDefault();
			if (target == null)
				return false;

			if (!CheckIsDirectoryValid(targetConnectionId, targetDirectory))
				return false;
			string filename;
			int multiplicator;
			switch (fileType)
			{
				case FileType.Fingerprint:
					filename = string.Format("{0}.FPRINT", _aso.AgentInDb.DisplayName);
					multiplicator = 5;
					break;
				case FileType.Retina:
					filename = string.Format("{0}.RSCAN", _aso.AgentInDb.DisplayName);
					multiplicator = 10;
					break;
				case FileType.Dna:
					filename = string.Format("{0}.DNA", _aso.AgentInDb.DisplayName);
					multiplicator = 40;
					break;
				case FileType.Psec:
					filename = string.Format("{0}.PSEC", _aso.AgentInDb.DisplayName);
					multiplicator = 80;
					break;
				case FileType.Tbpa:
					filename = string.Format("{0}.TBPA", _aso.AgentInDb.DisplayName);
					multiplicator = 240;
					break;
				default:
					return false;
			}

			if (targetConnectionId == Guid.Empty)
			{
				if (!HasEnoughMemory(multiplicator))
					return false;
				SystemFile dupeCheck = (from SystemFile x in Db where !x.IsDeleted && x.Type == fileType && x.Name == filename select x).FirstOrDefault();
				if (dupeCheck != null)
					return false;
			}

			SystemFile credFile = new SystemFile
			{
				Id = Guid.NewGuid().ToLong(),
				CompressionLevel = 0,
				Content = null,
				CopyOf = null,
				DisplaySize = multiplicator,
				EncryptionLevel = 0,
				Name = filename,
				Size = multiplicator,
				Time = World.Instance.WorldTime,
				Type = fileType,
				ParentId = target.Id
			};
			Db.PersistEntity(credFile);
			Db.Commit();
			return true;
		}

		private bool HasEnoughMemory(int requiredSize)
		{
			if (_aso == null)
				return false;
			int totalMem = _aso.AgentInDb.Gateway.GetTotalMemory();
			int usedMem = _aso.AgentInDb.Gateway.ComputerSystem.RootDirectory.GetUsedMemory(Db);
			if ((totalMem - usedMem) >= requiredSize)
				return true;
			return false;
		}

		private double GetCompressionModifier()
		{
			return _version * 1.2;
		}

		private bool IsSourceTargetConnectionValid(Guid sourceId, Guid targetId)
		{
			if (sourceId != Guid.Empty && targetId != Guid.Empty)
				return false;
			return true;
		}

		private bool CheckIsFileValid(Guid connectionId, ulong fileId)
		{
			if (connectionId == Guid.Empty)
				return true;

			AgentConnection conn = _aso.GetAgentConnection(connectionId);
			if (conn == null)
				return false;

			ulong targetId = conn.TargetSystemId;
			var check = (from SystemFile f in Db where f.Id == fileId
						 from ComputerSystem cs in Db
						 where cs.Id == targetId && cs.RootDirectory != null &&
						 cs.RootDirectory.Id == f.ParentId
						 select f).FirstOrDefault();
			if (check == null)
				return false;
			return true;
		}

		private bool CheckIsDirectoryValid(Guid connectionId, ulong directoryId)
		{
			if (connectionId == Guid.Empty)
				return true;
			if (directoryId != 0)
			{
				AgentConnection conn = _aso.GetAgentConnection(connectionId);
				if (conn == null)
					return false;
				ulong targetId = conn.TargetSystemId;
				var check = (
					from ComputerSystem cs in Db
					where cs.Id == targetId && cs.RootDirectory != null && cs.RootDirectory.Id == directoryId
					select cs.RootDirectory
					).FirstOrDefault();

				if (check == null)
					return false;
			}
			return true;
		}

		private bool CheckIsLogValid(Guid connectionId, ulong logId)
		{
			if (connectionId == Guid.Empty)
				return true;

			AgentConnection conn = _aso.GetAgentConnection(connectionId);
			if (conn == null)
				return false;

			ulong targetId = conn.TargetSystemId;
			var check = (from ComputerSystemLog csl in Db
						 where csl.Id == logId && csl.LoggingComputerSystemId == targetId
						 select csl).FirstOrDefault();
			if (check == null)
				return false;
			return true;
		}

		#endregion
	}

}